package org.durandj.ystream.client;

import java.util.ArrayList;
import java.util.List;

import javax.swing.JOptionPane;

import org.durandj.ystream.client.message.InvalidMessageContentsException;
import org.durandj.ystream.client.message.Message;
import org.durandj.ystream.client.message.MessageHandler;
import org.durandj.ystream.client.util.ByteBufferUtils;

import uk.co.caprica.vlcj.player.MediaPlayer;
import uk.co.caprica.vlcj.player.MediaPlayerEventListener;
import uk.co.caprica.vlcj.player.MediaPlayerFactory;
import uk.co.caprica.vlcj.player.VideoMetaData;
import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer;

/**
 * Controls the media player part of the client. This also provides message handling capabilities to the
 * VLCJ player so that it can interface with different parts of the client.
 * 
 * @author durandj
 * @version 1.0
 *
 */
public class YStreamPlayer extends MessageHandler
{
	/**
	 * An adapter class for listening to media player events. With this class you don't need
	 * to define a method for every media player event.
	 * 
	 * @author durandj
	 * @version 1.0
	 *
	 */
	public static class MediaPlayerEventAdapter implements MediaPlayerEventListener
	{
		/* (non-Javadoc)
		 * @see uk.co.caprica.vlcj.player.MediaPlayerEventListener#error(uk.co.caprica.vlcj.player.MediaPlayer)
		 */
		@Override
		public void error(MediaPlayer mp)
		{
		}

		/* (non-Javadoc)
		 * @see uk.co.caprica.vlcj.player.MediaPlayerEventListener#finished(uk.co.caprica.vlcj.player.MediaPlayer)
		 */
		@Override
		public void finished(MediaPlayer mp)
		{
		}

		/* (non-Javadoc)
		 * @see uk.co.caprica.vlcj.player.MediaPlayerEventListener#lengthChanged(uk.co.caprica.vlcj.player.MediaPlayer, long)
		 */
		@Override
		public void lengthChanged(MediaPlayer mp, long length)
		{
		}

		/* (non-Javadoc)
		 * @see uk.co.caprica.vlcj.player.MediaPlayerEventListener#mediaChanged(uk.co.caprica.vlcj.player.MediaPlayer)
		 */
		@Override
		public void mediaChanged(MediaPlayer mp)
		{
		}

		/* (non-Javadoc)
		 * @see uk.co.caprica.vlcj.player.MediaPlayerEventListener#metaDataAvailable(uk.co.caprica.vlcj.player.MediaPlayer, uk.co.caprica.vlcj.player.VideoMetaData)
		 */
		@Override
		public void metaDataAvailable(MediaPlayer mp, VideoMetaData metadata)
		{
		}

		/* (non-Javadoc)
		 * @see uk.co.caprica.vlcj.player.MediaPlayerEventListener#paused(uk.co.caprica.vlcj.player.MediaPlayer)
		 */
		@Override
		public void paused(MediaPlayer mp)
		{
		}

		/* (non-Javadoc)
		 * @see uk.co.caprica.vlcj.player.MediaPlayerEventListener#playing(uk.co.caprica.vlcj.player.MediaPlayer)
		 */
		@Override
		public void playing(MediaPlayer mp)
		{
		}

		/* (non-Javadoc)
		 * @see uk.co.caprica.vlcj.player.MediaPlayerEventListener#positionChanged(uk.co.caprica.vlcj.player.MediaPlayer, float)
		 */
		@Override
		public void positionChanged(MediaPlayer mp, float position)
		{
		}

		/* (non-Javadoc)
		 * @see uk.co.caprica.vlcj.player.MediaPlayerEventListener#stopped(uk.co.caprica.vlcj.player.MediaPlayer)
		 */
		@Override
		public void stopped(MediaPlayer mp)
		{
		}

		/* (non-Javadoc)
		 * @see uk.co.caprica.vlcj.player.MediaPlayerEventListener#timeChanged(uk.co.caprica.vlcj.player.MediaPlayer, long)
		 */
		@Override
		public void timeChanged(MediaPlayer mp, long time)
		{
		}
		
	}
	
	/**
	 * This is a value for the <code>current</code> member of this class.
	 * It is used to tell the player that nothing is playing and when the player
	 * does start it should start at the beginning of the playlist.
	 */
	private static final int NOT_PLAYING_ANYTHING = -1;
	
	/**
	 * The VLCJ media player reference.
	 */
	private EmbeddedMediaPlayer mediaPlayer;
	
	/**
	 * Listens to the player for specific events.
	 */
	private MediaPlayerEventListener listener;
	
	/**
	 * The list of files in the playlist.
	 */
	private List<String> playlist;
	
	/**
	 * Flag telling the player to stay alive.
	 */
	private Boolean alive;
	
	/**
	 * Flag for media being paused.
	 */
	private Boolean isPaused;
	
	/**
	 * The currently playing item in the playback queue.
	 */
	private Integer current;
	
	/**
	 * Whether or not the system is muted.
	 * @author nickiannone
	 */
	private Boolean isMuted;
	
	/**
	 * The previous volume.
	 * @author nickiannone
	 */
	private Integer volume;
	
	/**
	 * The thread of execution.
	 */
	private Thread thread;
	
	/**
	 * Creates the YStreamPlayer and does basic setup.
	 */
	public YStreamPlayer()
	{
		MediaPlayerFactory factory = new MediaPlayerFactory(new String[] { });
		this.mediaPlayer = factory.newMediaPlayer(null);
		this.playlist = new ArrayList<String>();
		this.alive = true;
		this.isPaused = false;
		// Added by nickiannone
		this.isMuted = false;
		this.volume = mediaPlayer.getVolume();
		this.current = NOT_PLAYING_ANYTHING;
		
		// TODO: If some of these methods are now used they should be commented as such.
		// TODO: At some point maybe make a new class that does not require all the methods to be implemented.
		this.listener = new MediaPlayerEventAdapter() {
			@Override
			public void finished(MediaPlayer arg0)
			{
				if(current == playlist.size() - 1)
				{
					current = NOT_PLAYING_ANYTHING;
					return;
				}
				
				setCurrentlyPlaying(++current);
				mediaPlayer.playMedia(playlist.get(current));
			}
		};
		this.mediaPlayer.addMediaPlayerEventListener(listener);
		
		this.thread = new Thread(new Runnable() {
			@Override
			public void run()
			{
				while(alive)
				{
					if(noMessages())
						waitForMessage();
					
					Message msg = nextMessage();
					handleMessage(msg);
				}
				
				// Let's stop the player if it's got something going.
				if(isPaused || mediaPlayer.isPlaying())
					mediaPlayer.stop();
				
				// Clean things up a bit...
				mediaPlayer.release();
			}
		}, "YStream Player");
		this.thread.start();
	}
	
	/**
	 * Checks if the player is alive.
	 * 
	 * @return is it alive?
	 */
	public boolean isAlive()
	{
		return thread.isAlive();
	}
	
	/**
	 * Sets the video surface of the player.
	 * 
	 * @param videoSurface - the surface to use for video.
	 */
	public void setVideoSurface(java.awt.Canvas videoSurface)
	{
		if(videoSurface == null)
			throw new NullPointerException("Must give a valid video surface.");
		
		// TODO: The player may need to be paused first...
		
		synchronized(mediaPlayer) {
			mediaPlayer.setVideoSurface(videoSurface);
		}
	}
	
	/**
	 * Checks if the player is currently playing something.
	 * 
	 * @return is the player playing?
	 */
	public boolean isPlaying()
	{
		synchronized(mediaPlayer) {
			return mediaPlayer.isPlaying();
		}
	}
	
	/**
	 * Checks if the player is currently paused.
	 * 
	 * @return is it paused?
	 */
	public boolean isPaused()
	{
		synchronized(isPaused) {
			return isPaused;
		}
	}
	
	/**
	 * Checks if the player is currently stopped.
	 * 
	 * @return is it stopped?
	 */
	public synchronized boolean isStopped()
	{
		return !isPaused() && !isPlaying();
	}
	
	/**
	 * Gets the index of the currently playing item in the playlist.
	 * 
	 * @return current play index
	 */
	public int getCurrentlyPlaying()
	{
		synchronized(current) {
			return current;
		}
	}
	
	/**
	 * Gets the current position in the currently playing selection.
	 * 
	 * @return the position
	 */
	public float getPosition()
	{
		synchronized(mediaPlayer) {
			return mediaPlayer.getPosition();
		}
	}
	
	/**
	 * Changes the currently playing media. If something is currently playing then this
	 * method will stop the player and then play the new selection.
	 * 
	 * @param index - index of an item in the playlist
	 */
	public void setCurrentlyPlaying(int index)
	{
		if(index < 0 || index >= playlist.size())
			throw new IndexOutOfBoundsException("Index " + index + " does not exist.");
		
		current = index;
		
		if(isPlaying())
		{
			mediaPlayer.stop();
			mediaPlayer.playMedia(playlist.get(current));
		}
	}
	
	/**
	 * Gets the current playlist of the player.
	 * 
	 * @return the player's playlist
	 */
	public List<String> getPlaylist()
	{
		return playlist;
	}
	
	/**
	 * Tells whether or not the player is muted.
	 * @author nickiannone
	 * @return <b>true</b> if the player is muted, <b>false</b> otherwise.
	 */
	public boolean isMuted() {
		return isMuted;
	}
	
	/**
	 * Mutes the volume.
	 * @author nickiannone
	 * @TODO Make this function update the mute button icon on the GUI?
	 * @TODO Make the GUI actually _have_ a mute button?
	 */
	public void mute() {
		isMuted = true;
		log.info("Muting.\n");
		mediaPlayer.setVolume(0);
	}
	
	/**
	 * Unmutes the volume.
	 * @author nickiannone
	 * @TODO Make this function update the mute button icon on the GUI?
	 * @TODO Make the GUI actually _have_ a mute button?
	 */
	public void unmute() {
		isMuted = false;
		log.info("Unmuting, volume set to " + volume + ".\n");
		mediaPlayer.setVolume(volume);
	}
	
	/**
	 * Sets the volume of the system, unmuting if the player is muted.
	 * @param vol The volume (clamped between 0-200) to set.
	 * @author nickiannone
	 * @TODO Make this function update the mute button icon on the GUI?
	 * @TODO Make the GUI actually _have_ a mute button?
	 */
	public void setVolume(int vol) {
		// Clamp the volume to 0-200.
		if (vol < 0) {
			vol = 0;
		}
		if (vol > 200) {
			vol = 200;
		}
		// Store the volume.
		volume = vol;
		if (isMuted() && vol != 0) {
			// Unmute and set if needed; done through the unmute() method.
			unmute();
		} else {
			// Otherwise, set the volume and log the change.
			mediaPlayer.setVolume(volume);
			log.info("Volume set to " + vol + ".\n");
		}
	}
	
	/**
	 * Subscribes to player events.
	 * 
	 * @param listener - a listener
	 */
	public void subscribe(MediaPlayerEventListener listener)
	{
		if(listener == null)
			throw new NullPointerException("Must give a valid listener.");
		
		mediaPlayer.addMediaPlayerEventListener(listener);
	}
	
	/**
	 * Unsubscribes to player events.
	 * 
	 * @param listener - a listener
	 */
	public void unsubscribe(MediaPlayerEventListener listener)
	{
		if(listener == null)
			throw new NullPointerException("Must give a valid listener.");
		
		mediaPlayer.removeMediaPlayerEventListener(listener);
	}

	/* (non-Javadoc)
	 * @see org.durandj.ystream.client.message.MessageHandler#handleMessage(org.durandj.ystream.client.message.Message)
	 */
	@Override
	protected void handleMessage(Message msg)
	{
		switch(msg.getID())
		{
		case KILL:
			synchronized(alive) {
				alive = false;
			}
			break;
			
		case PLAYER_PAUSE:
			if(!mediaPlayer.isPlaying())
				return;
			
			mediaPlayer.pause();
			isPaused = true;
			break;
			
		case PLAYER_PLAY:
		{
			// TODO: This should have the option to take a parameter.
			
			// Check if there's something in the playlist to play.
			if(playlist.isEmpty())
			{
				JOptionPane.showMessageDialog(YStreamClient.getClient().getWindow(),
				                              "Must have something in the playlist.",
				                              "Playlist is empty",
				                              JOptionPane.ERROR_MESSAGE);
				return;
			}
			
			if(isPaused)
				mediaPlayer.play();
			else if(current == NOT_PLAYING_ANYTHING)
				mediaPlayer.playMedia(playlist.get(current = 0));
			else
				mediaPlayer.playMedia(playlist.get(current));
			isPaused = false;
			break;
		}
			
		case PLAYER_STOP:
			if(!mediaPlayer.isPlaying() && !isPaused)
				return;
			
			mediaPlayer.stop();
			isPaused = false;
			break;
			
		case PLAYLIST_ADD:
		{
			if(msg.getContents() == null)
				throw new InvalidMessageContentsException("Must specify file name.");
			
			String filename = ByteBufferUtils.getStringFromByteBuffer(msg.getContents());
			if(filename.isEmpty())
				throw new InvalidMessageContentsException("Must specify file name.");
			
			playlist.add(filename);
			
			log.info("File added to playlist:\n" +
			         "\t" + filename);
			break;
		}
			
		case PLAYLIST_REMOVE:
		{
			if(msg.getContents() == null)
				throw new InvalidMessageContentsException("Must specify the index of the playlist item.");
			
			int index = msg.getContents().getInt();
			
			if(index < 0 || index >= playlist.size())
				throw new InvalidMessageContentsException("Must specify the index of the element in the playlist.");
			
			String filename = playlist.remove(index);
			
			log.info("File removed from playlist:\n" +
			         "\t" + filename);
			break;
		}
		
		case PLAYLIST_CLEAR:
		{
			// TODO: Maybe stopping of the player should be done here...
			playlist.clear();
			break;
		}
			
		case PLAYLIST_UP:
		{
			// TODO: Error checking should be done?
			
			if(msg.getContents() == null)
				throw new InvalidMessageContentsException("Must specify the index of the playlist item.");
			
			int index = msg.getContents().getInt();
			
			if(index < 0 || index >= playlist.size())
				throw new InvalidMessageContentsException("Must specify the index of the element in the playlist.");
			
			String filename = playlist.get(index);
			playlist.set(index, playlist.get(index - 1));
			playlist.set(index - 1, filename);
			
			log.info("File moved up in the playlist:\n" +
					 "\t" + filename);
			break;
		}
			
		case PLAYLIST_DOWN:
		{
			// TODO: Error checking should be done?
			
			if(msg.getContents() == null)
				throw new InvalidMessageContentsException("Must specify the index of the playlist item.");
			
			int index = msg.getContents().getInt();
			
			if(index < 0 || index >= playlist.size())
				throw new InvalidMessageContentsException("Must specify the index of the element in the playlist.");
			
			String filename = playlist.get(index);
			playlist.set(index, playlist.get(index + 1));
			playlist.set(index + 1, filename);
			
			log.info("File moved down in the playlist:\n" +
					 "\t" + filename);
			break;
		}
			
		case PLAYLIST_NEXT:
		{
			// Check if we're already at the end of the playlist.
			if(current == playlist.size() - 1)
				return;
			
			// Check if the player is playing and if so stop it.
			if(mediaPlayer.isPlaying() || isPaused)
				mediaPlayer.stop();
			
			current++;
			mediaPlayer.playMedia(playlist.get(current));
			break;
		}
			
		case PLAYLIST_BACK:
		{
			// Check if we're already at the beginning of the playlist.
			if(current == 0)
				return;
			
			// Check if the player is playing and if so stop it.
			if(mediaPlayer.isPlaying() || isPaused)
				mediaPlayer.stop();
			
			current--;
			mediaPlayer.playMedia(playlist.get(current));
			break;
		}
			
		default:
			// TODO: Do something to notify of an unhandled message.
			log.warning("Unhandled message!\n" +
						"\tID: " + msg.getID() + "\n" +
						"\tSender: " + msg.getSender().getClass().getSimpleName() + "\n");
			break;
		}
	}
}
